package org.gienah.testing.junit;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.gienah.testing.xml.DependencyDefinition;
import org.gienah.testing.xml.DependentDefinition;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @author Santiago L. Valdarrama
 * @since E0.1
 */
public final class Injector {

	private static final Log logger = LogFactory.getLog(Injector.class);

	private Injector() {
	}

	public static void inject(final DependentDefinition dependentDefinition, final Object object, final Class<?> clazz,
			final ConfigurableApplicationContext context)
			throws Exception {

		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			if ((object == null && Modifier.isStatic(field.getModifiers())) || object != null) {
				processDependentDefinitionDependencies(dependentDefinition, field, object, context);
				processDependentDefinitionContext(dependentDefinition, field, object, context);
				processAnnotation(field.getAnnotation(Dependency.class), field, object, context);
				processAnnotation(field.getAnnotation(Context.class), field, object, context);
			}
		}
	}

	private static void processDependentDefinitionDependencies(final DependentDefinition dependentDefinition, final Field field, final Object object,
			final ConfigurableApplicationContext context)
			throws Exception {

		if (dependentDefinition != null) {
			DependencyDefinition dependency = dependentDefinition.getDependency(field.getName());
			if (dependency != null) {
				if (context.containsBean(dependency.getBean())) {
					assignField(object, field, context.getBean(dependency.getBean()));
				}
				else {
					assignFieldUsingType(field, object, context);
				}
			}
		}
	}

	private static void processDependentDefinitionContext(final DependentDefinition dependentDefinition, final Field field, final Object object,
			final ConfigurableApplicationContext context)
			throws Exception {

		if (dependentDefinition != null && dependentDefinition.isContextField(field.getName())) {
			assignField(object, field, context);
		}
	}

	private static void processAnnotation(final Dependency annotation, final Field field, final Object object,
			final ConfigurableApplicationContext context)
			throws Exception {

		if (annotation != null) {
			String bean = StringUtils.isEmpty(annotation.bean())
					? field.getName()
					: annotation.bean();

			if (context.containsBean(bean)) {
				assignField(object, field, context.getBean(bean));
			}
			else {
				assignFieldUsingType(field, object, context);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static void assignFieldUsingType(final Field field, final Object object, final ConfigurableApplicationContext context)
			throws DependencyException, IllegalArgumentException, IllegalAccessException {

		Map beans = context.getBeansOfType(field.getType());
		if (beans.size() == 1) {
			assignField(object, field, beans.values().iterator().next());
		}
		else if (beans.size() > 1) {
			throw new DependencyException("There are " + beans.size() + " beans of type \"" + field.getType().getName() + "\" to assign the \""
					+ field.getName() + "\" property of \"" + object.getClass().getName()
					+ "\" class. There should have been exactly 1 to be able to automatically assign the field.");
		}
		else {
			if (Injector.logger.isWarnEnabled()) {
				Injector.logger.warn("A matching bean to property \"" + field.getName() + "\" in class \"" + object.getClass().getName()
						+ "\" was not found.");
			}
		}
	}

	private static void processAnnotation(final Context annotation, final Field field, final Object object,
			final ConfigurableApplicationContext context)
			throws Exception {

		if (annotation != null) {
			assignField(object, field, context);
		}
	}

	private static void assignField(final Object object, final Field field, final Object value)
			throws IllegalArgumentException, IllegalAccessException {

		field.setAccessible(true);
		field.set(object, value);
	}
}
